Repository navigation
Conversation
This comment was marked as outdated.
This comment was marked as outdated.
Documentation build overview
45 files changed ·
|
f3f2dfe to
2950322
Compare
…e polling for I/O After running the ready callbacks, BaseEventLoop._run_once() now keeps running the callbacks they schedule, in batches, before it polls for I/O again. This saves one I/O poll per hop of a callback chain, for example when a completed future wakes up a task. Eager processing is bounded by the new asyncio.EventLoopSettings, set with loop.set_settings() and read with loop.get_settings(): eager_max_time (default 100 microseconds) and eager_max_callbacks (default 100). asyncio.sleep(0) ends eager processing, so that the task resumes only after the next I/O poll. When stop() is called during eager processing, run_forever() runs one more iteration, which polls for I/O and runs what the loop would have run without eager processing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts: # Doc/whatsnew/3.16.rst
| if left > 0: | ||
| left = min(left, ntodo) | ||
| self._run_handles(left, eager=False) | ||
| if self._stopping: |
There was a problem hiding this comment.
I don't understand why this test is present here.
There was a problem hiding this comment.
- The previous iteration didn't process all
_readyhandlers because of limits.loop.stop()was not called. - During execution of these pending timeouts, one of the callbacks has called
loop.stop() - To not break
run_until_complete()logic, we should stop the iteration after executing callbacks from this pack and stop. If we remove the check, extra callbacks (timers, I/O) could be called. It doesn't change any pre-existing tests but behaves a little differently than the 'main' version.
Because of this, I've preferred to keep this check and add a test for pinning the behavior.
There was a problem hiding this comment.
Thank for the explanation, I forgot the previous iteration. Last question: I wonder why the self._eager_state attr is not update with ntodo - nleft before the return statement as you do it in line 2110 and 2129.
There was a problem hiding this comment.
Good question, thanks!
The PR keeps a negative (-1 or -2) _eager_state on a purpose.
If we stored a count, run_forever() would see _stopping and _eager_state >= 0 and run the extra final _run_once() (https://github.com/python/cpython/pull/145149/files#diff-7962e1627bc1bfbce13bef0be78d5ef7fc962f99bbc524a199ef489852ca1344R689). That would run callbacks from the next iteration, which breaks a code that relies on existing run_until_complete() behavior.
With negative _eager_state, run_forever() just breaks. The remaining callbacks stay queued and run the next time the loop runs.
There was a problem hiding this comment.
Thanks for your reply. It was hard for me to understand how self._state_eager can behave sometimes as a state and sometimes as a counter. Everything is clear to me now.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| if (not isinstance(self.eager_max_time, (int, float)) | ||
| or isinstance(self.eager_max_time, bool)): | ||
| raise TypeError(f"eager_max_time must be an int or float, " | ||
| f"got {self.eager_max_time!r}") |
There was a problem hiding this comment.
As you check attribute type, it is more appropriate to print the bad type that raises the exception.
I suggest f"got {type(self.eager_max_time).__name__!r}".
Same remark applies to self.eager_max_callbacks line 266
|
|
||
|
|
||
| EventLoopSettings.DEFAULT = EventLoopSettings(eager_max_time=1e-4, | ||
| eager_max_callbacks=100) |
There was a problem hiding this comment.
Replace 100 with EAGER_BATCH_SIZE ?
| # of the eager_max_time deadline. A batch also ends where the callbacks | ||
| # scheduled during it begin, so for chains of callbacks the deadline is | ||
| # checked after every callback. | ||
| EAGER_BATCH_SIZE = 100 |
There was a problem hiding this comment.
Does adding the constant EAGER_MAX_TIME = 1e-4 make sense?
Run callbacks that are scheduled during an event loop iteration eagerly, within a small budget, instead of always waiting for the next I/O poll. This removes one
select()/epoll_wait()call per hop in code that resolves futures or wakes tasks in chains.asyncio.EventLoopSettings(eager_max_time, eager_max_callbacks)andloop.get_settings()/loop.set_settings(). The defaults are 100 µs and 100 callbacks;eager_max_time=0turns eager processing off.asyncio.sleep(0), which still guarantees an I/O poll, and onloop.stop(), which keeps its documented "current batch only" behaviour.Benchmarks (release build, script): a
call_soon()chain is 38% faster,Queueping-pong 25% faster, and a task awaiting a child task 33% faster. Servers with many connections are unchanged. While a long run of callbacks keeps going, timers and I/O can be delayed by up to about 100–200 µs.A few tests that assumed "one task hop = one I/O poll" were adjusted.